/**
 * @description 富文本HTML解析器（包含清理、转换和分割）
 * 这是一个高度健壮的解析器，它将从富文本编辑器（如Quill.js）生成的、可能带有预设样式的HTML字符串，
 * 转换为一个结构清晰、样式纯净、适用于小程序 rich-text 组件或其他自定义渲染环境的节点数组。
 *
 * @strategy 核心策略是“先清理，后添加”：
 * 1.  清理（Cleanup）: 主动移除源HTML中所有不需要的预设样式（如 text-indent）。
 * 2.  添加（Augment）: 根据我们自己的规则（如 \t 制表符），精确地添加我们需要的样式（如 margin-left）。
 * 这种策略确保了输出样式的绝对可控性，避免了被源HTML的“脏样式”污染。
 *
 * @workflow 工作流程:
 * 1. 【样式预转换】: 通过正则表达式查找特定的CSS类名（如 'ql-size-*'），并将其转换为对应的内联 `font-size` 样式。
 * 2. 【迭代分割节点】: 采用最健壮的“迭代匹配”策略（while + regex.exec），以媒体标签（img, iframe）为锚点，
 *    精准地将HTML分割成连续的文本节点和独立的媒体节点，此方法避免了 `split()` 函数会破坏HTML结构的问题。
 * 3. 【文本节点处理】: 对每个提取出的文本块，执行“先清理，后添加”策略。
 *    a. 全局清理: 移除所有 `text-indent` 样式。
 *    b. 精确添加: 只为内容以 `\t` 开头的段落添加 `margin-left` 缩进。
 * 4. 【结构化封装】: 将所有处理好的节点封装成 `{ type: '...', ... }` 格式的对象，并存入数组中返回。
 *
 * @param {string} htmlString - 带有预设样式的原始HTML字符串。
 * @returns {Array<Object>} 一个样式纯净、结构正确的节点数组。
 *   例如: [
 *     { type: 'text', content: '<p>处理后的文本</p>' },
 *     { type: 'img', src: '...' },
 *     { type: 'video', src: '...' }
 *   ]
 */
export function parseRichTextToNodes(htmlString) {
	// --- 输入验证 (Robustness Check) ---
	// 确保输入是有效的非空字符串，防止后续操作因null或undefined等无效输入而崩溃。
	if (!htmlString || typeof htmlString !== 'string') {
		return [];
	}

	// --- 阶段一: 样式预转换 ---
	// 此阶段的目标是将富文本编辑器生成的特定CSS类名，转换为小程序等环境更易于支持的内联style。
	let processedHtml = htmlString;

	// 定义一个从CSS类名到具体style值的映射表，便于管理和扩展。
	const FONT_SIZE_MAP = {
		'ql-size-small': '10px',
		'ql-size-large': '18px',
		'ql-size-huge': '32px',
	};

	// 正则表达式，用于匹配所有 <span> 和 <h1>-<h6> 标签，并捕获其标签名和所有属性。
	// 捕获组1: (span|h[1-6]) -> 标签名
	// 捕获组2: ([^>]*)      -> 标签内的所有属性字符串
	const classRegex = /<(span|h[1-6])([^>]*)>/gi;
	processedHtml = processedHtml.replace(classRegex, (match, tagName, attributes) => {
		let targetFontSize = null;
		// 遍历映射表，检查当前标签的属性中是否包含我们需要转换的类名。
		for (const className in FONT_SIZE_MAP) {
			if (attributes.includes(className)) {
				targetFontSize = FONT_SIZE_MAP[className];
				break; // 找到后立即退出循环
			}
		}

		// 如果没有找到任何匹配的字体类名，则不进行任何修改，返回原始标签。
		if (!targetFontSize) return match;

		// 如果找到了，我们将构建新的属性字符串。
		let finalAttributes = attributes;
		const styleRegex = /style="([^"]*)"/i; // 用于查找已存在的style属性

		// 检查标签是否已经有style属性
		if (styleRegex.test(attributes)) {
			// 如果有，就在现有样式的前面追加新的字体大小样式。
			// 这样做可以避免覆盖掉用户可能已经设置的其他内联样式（如 color）。
			finalAttributes = attributes.replace(styleRegex, (styleMatch, existingStyles) => {
				return `style="font-size: ${targetFontSize}; ${existingStyles}"`;
			});
		} else {
			// 如果没有，就直接添加一个新的style属性。
			finalAttributes += ` style="font-size: ${targetFontSize};"`;
		}

		// 返回带有新内联样式的、重新构建好的标签。
		return `<${tagName}${finalAttributes}>`;
	});

	// --- 阶段二: 节点分割与解析 ---
	// 这是整个函数最核心的部分，采用“迭代匹配”策略来安全地分割HTML。
	const nodes = [];
	// 正则表达式，用于查找被<p>包裹或独立的媒体标签(iframe, img)。
	// (?:<p>)? : 一个非捕获组，匹配可选的<p>标签，因为有些编辑器会自动包裹媒体。
	// \s*       : 匹配可选的空白字符。
	// (<iframe...|...>) : 核心捕获组1，匹配iframe或img标签本身。
	const mediaRegex = /(?:<p>)?\s*(<iframe[\s\S]*?<\/iframe>|<img[^>]*>)\s*(?:<\/p>)?/gi;

	let lastIndex = 0; // 记录上一次匹配结束的位置，作为下一次文本截取的起点。
	let match; // 存储每次匹配的结果。

	// 内部辅助函数：从HTML标签字符串中安全地提取指定属性的值。
	const getAttribute = (tagString, attributeName) => {
		// 这个正则表达式可以处理双引号、单引号和无引号的属性值。
		const regex = new RegExp(`${attributeName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i');
		const match = tagString.match(regex);
		return match ? (match[1] || match[2] || match[3]) : null;
	};

	// 内部辅助函数：处理所有文本节点的总入口，包含“清理”和“添加”两步。
	const processTextNode = (text) => {
		// 忽略空的或只包含空白的文本块。
		if (!text || !text.trim()) {
			return;
		}
		let textContent = text.trim();

		// --- 步骤 3a: 全局清理 (Cleanup) ---
		// 这个正则表达式查找并移除所有 `text-indent` 样式声明。
		// `text-indent:` : 匹配字面量
		// `\s*`        : 匹配任意数量的空白
		// `[^;]+`      : 匹配一个或多个非分号的字符（即样式值）
		// `;?`         : 匹配一个可选的分号
		// `g`          : 全局匹配，确保清理所有实例
		const cleanupIndentRegex = /text-indent:\s*[^;]+;?\s*/g;
		textContent = textContent.replace(cleanupIndentRegex, '');

		// (可选) 额外的清理步骤：如果清理后 style 属性变为空 (如 style=" ")，也把它彻底移除，保持HTML整洁。
		textContent = textContent.replace(/style="\s*"/g, '');

		// --- 步骤 3b: 精确添加 (Augment) ---
		// 在清理干净的HTML上，执行我们自定义的 `\t` 缩进逻辑。
		// 捕获组1: (p|li)       -> 块级标签名
		// 捕获组2: ([^>]*)      -> 标签属性
		// 捕获组3: (\t+)        -> 一个或多个制表符
		// 捕获组4: ([\s\S]*?)   -> 标签内容 (非贪婪匹配)
		// \1: 反向引用，确保闭合标签与起始标签一致
		const addIndentRegex = /<(p|li)([^>]*)>(\t+)([\s\S]*?)<\/\1>/gi;
		textContent = textContent.replace(addIndentRegex, (match, tagName, attributes, tabs, content) => {
			const indentLevel = tabs.length; // 根据制表符数量计算缩进级别
			const indentSize = indentLevel * 2; // 每级缩进2em
			let newAttributes = attributes;
			const styleRegex = /style="([^"]*)"/i;

			if (styleRegex.test(attributes)) {
				// 如果已存在style，注入margin-left
				newAttributes = attributes.replace(styleRegex, (styleMatch, existingStyles) => {
					return `style="margin-left: ${indentSize}em; ${existingStyles}"`;
				});
			} else {
				// 否则，创建新的style属性
				newAttributes += ` style="margin-left: ${indentSize}em;"`;
			}
			// 返回重新构建的、带有缩进样式的HTML标签
			return `<${tagName}${newAttributes}>${content.trim()}</${tagName}>`;
		});

		// 将处理完毕的文本块作为一个节点推入结果数组
		nodes.push({
			type: 'text',
			content: textContent
		});
	};

	// --- 阶段三: 循环匹配与分割 ---
	// 使用 while 循环和 exec 方法，这是处理此类解析任务最健壮的方式。
	while ((match = mediaRegex.exec(processedHtml)) !== null) {
		// 1. 添加从上一个媒体到当前媒体之间的所有内容，作为文本节点。
		const textBefore = processedHtml.substring(lastIndex, match.index);
		processTextNode(textBefore);

		// 2. 处理当前匹配到的媒体节点。
		const mediaTag = match[1]; // match[1] 是我们正则中定义的干净的媒体标签捕获组
		if (mediaTag.toLowerCase().startsWith('<iframe')) {
			const src = getAttribute(mediaTag, 'src');
			if (src) nodes.push({
				type: 'video',
				src: src
			});
		} else if (mediaTag.toLowerCase().startsWith('<img')) {
			const src = getAttribute(mediaTag, 'src');
			if (src) nodes.push({
				type: 'img',
				src: src
			});
		}

		// 3. 更新下一次搜索的起始位置，这对于循环至关重要。
		lastIndex = mediaRegex.lastIndex;
	}

	// --- 阶段四: 处理收尾文本 ---
	// 添加最后一个媒体标签之后的所有剩余文本。
	const remainingText = processedHtml.substring(lastIndex);
	processTextNode(remainingText);

	// console.log(JSON.stringify(nodes, null, 4));
	console.log(nodes);

	return nodes;
}